/**
 * @(#)ConfigurationWorker.java
 * 
 * @author Alan Salnikov
 * @author Mankin Yuen
 * @author William Clark
 */
package edg.acm;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import edg.db.Simulator;

/**
 * Provides the means to signal that the configuration is complete. It
 * validates, creates the XML files, and signals the Database module that there
 * is a configuration ready to be simulated.
 */
public class ConfigurationWorker {

	static Random random = new Random();
	static File USER_DIR = new File(System.getProperty("user.dir"));

	/**
	 * Validates the model using {@link ConfigurationModel validate()}, saves
	 * using {@link ConfigurationModel save()}, generates XML files as input to
	 * the simulator, and informs Module 3 that XML files are ready for the
	 * simulator.
	 * 
	 * @param cm
	 *            the {@link ConfigurationModel} to be submitted
	 * @return returns the ID of the configuration provided by the Database
	 * @throws ValidationException
	 *             thrown when an error during validation occurs
	 * @throws ConfigurationException
	 *             thrown when an error occurs during XML generation
	 */
	public static int runConfiguration(ConfigurationModel cm) throws ValidationException {
		cm.validate();
		cm.save();
		Files files = generateXML(cm);
		return Simulator.start(files, cm);
	}

	/**
	 * Generates the XML input for the simulator. Not intended for external use.
	 * 
	 * @param cm
	 *            The {@link ConfigurationModel} to have XML generated from
	 * @return a {@link Files} object with the paths to generated files
	 * @throws ConfigurationException
	 *             thrown when an error occurs during XML generation
	 */
	public static Files generateXML(ConfigurationModel cm) {
		try {

			File file = new File(System.getProperty("cs320.company1.cm.romeo_template", "romeo-config.xml"));
			Properties props = loadProperties(file);
			System.getProperties().putAll(props);
			String config_path = props.getProperty("laser.romeo.config.path");
			File dir;
			if (config_path == null) {
				props.setProperty("laser.romeo.config.path", USER_DIR.getAbsolutePath());
				dir = USER_DIR;
			} else {
				dir = new File(config_path);
			}

			if (!dir.exists())
				dir.mkdirs();

			File simulation = generateSimulation(cm, dir);
			File resources = generateResources(cm, dir);
			File allocation = File.createTempFile("Allocation", ".txt", dir);
			File utilization = File.createTempFile("ResUtilization", ".txt", dir);
			props.setProperty("laser.romeo.res-availability.file", resources.getName());
			props.setProperty("laser.romeo.allocation.file", allocation.getName());
			props.setProperty("laser.romeo.res-utilization", utilization.getName());
			File result = File.createTempFile("romeo-config", ".xml", dir);

			Files files = new Files();
			files.allocation = allocation.getAbsolutePath();
			files.utilization = utilization.getAbsolutePath();
			files.jsim = simulation.getAbsolutePath();
			files.romeo = result.getAbsolutePath();

			saveProperties(result, props);
			return files;
		} catch (IOException ex) {
			throw new ConfigurationException(ex.getMessage());
		}
	}

	/**
	 * Generates a JSim Oracle file
	 * 
	 * @param cm
	 *            the {@link ConfigurationModel} to have an Oracle file
	 *            generated from
	 * @param dir
	 *            the directory to create the Oracle file in
	 * @return the JSim Oracle {@link File}
	 * @throws ConfigurationException
	 *             thrown when an error occurs during XML generation
	 */
	static File generateSimulation(ConfigurationModel cm, File dir) {
		try {
			int rate = (Integer) cm.getValue("rate");
			int hours = (Integer) cm.getValue("hours");
			int[] patient_times = new int[rate];

			int[] acuities = new int[6];
			acuities[0] = (Integer) cm.getValue("acuity1");
			acuities[1] = (Integer) cm.getValue("acuity2");
			acuities[2] = (Integer) cm.getValue("acuity3");
			acuities[3] = (Integer) cm.getValue("acuity4");
			acuities[4] = (Integer) cm.getValue("acuity5");
			acuities[5] = (Integer) cm.getValue("acuity6");
			int walkin = (Integer) cm.getValue("walkin");

			Integer runs = (Integer) cm.getValue("runs");

			// temporary normalization logic
			// TODO: find an algorithm that will actually normalize to 100
			normalize(acuities, 100);

			//

			File file = new File(System.getProperty("cs320.company1.cm.jsim_template", "edcare.jsim"));

			Document doc = loadDocument(file);

			if (runs != null && runs != 1)
				doc.getRootElement().setAttribute("runs", runs.toString());

			get_or_make_child(doc.getRootElement(), "configuration");

			Element acu_prob = new Element("linear-probability");
			for (int i = 0; i < 6; i++)
				acu_prob.addContent(make_case(acuities[i], "patientInfo", "triageAcuity", "integer", i + 1));

			Element arr_prob = new Element("linear-probability");
			arr_prob.addContent(make_case(walkin, "patientInfo", "arrivalMode", "string", "WALKIN")).addContent(
					make_case(100 - walkin, "patientInfo", "arrivalMode", "string", "AMBULANCE"));

			Element rules = get_or_make_child(doc.getRootElement(), "rules");

			add_actions(
					get_or_make_child(
							get_or_make_step(rules, "SetPatientInfo"), "posted"),
							acu_prob, arr_prob);

			add_actions(
					get_or_make_child(
							get_or_make_step(rules, "EDRoot-reaction"), "started"),
							new Element("complete").addContent(new Element("fixed").
									setAttribute("value", Integer.toString(hours * 60))));
			add_actions(
					get_or_make_child(
							get_or_make_step(rules, "EDRoot-parallel"), "started"),
							new Element("complete").addContent(new Element("fixed").
									setAttribute("value", Integer.toString(hours * 60))));

			Element scenario = get_or_make_child(doc.getRootElement(), "scenario");
			scenario.removeContent();
			for (int i = 0; i < hours; i++) {
				int hour_start = i * 60;
				choose(patient_times, 60);
				for (int rel_time : patient_times) {
					scenario.addContent(new Element("message").setAttribute("type", "edparams.PatientArrivalMessage")
							.setAttribute("time", Integer.toString(hour_start + rel_time + 2)));
				}
			}

			File result = File.createTempFile("config", ".jsim", dir);
			saveDocument(result, doc);
			return result;
		} catch (JDOMException ex) {
			throw new ConfigurationException(ex);
		} catch (IOException ex) {
			throw new ConfigurationException(ex);
		}
	}

	/**
	 * Creates the &lt;case&gt; {@link Element} of a &lt;linear-probability&gt;
	 * {@link Element}, with a &lt;set-field&gt; action {@link Element}.
	 * 
	 * @param chance
	 *            the probability out of 100
	 * @param param
	 *            the name of the parameter to be modified
	 * @param name
	 *            the name of the field to modify
	 * @param type
	 *            the type of the field
	 * @param value
	 *            the value to change the field to
	 * @return the generated {@link Element}
	 */
	static Element make_case(int chance, String param, String name, String type, Object value) {
		return new Element("case").setAttribute("chance", Integer.toString(chance)).addContent(
				new Element("set-field").setAttribute("parameter", param).addContent(
						new Element("field").setAttribute("name", name).addContent(
								new Element(type).setAttribute("value", value.toString()))));
	}

	static Element get_or_make_child(Element parent, String name) {
		Element child = parent.getChild(name);
		if (child == null)
			parent.addContent(child = new Element(name));
		return child;
	}

	@SuppressWarnings("unchecked")
	static Element get_or_make_step(Element parent, String name) {
		Element step = null;
		for (Element cand : (List<Element>) parent.getChildren("step")) {
			if (cand.getAttributeValue("name").equals(name)) {
				step = cand;
				break;
			}
		}
		if (step == null)
			parent.addContent(step = new Element("step").setAttribute("name", name));
		return step;
	}

	static void add_actions(Element condition, Element... actions) {
		Element group = condition.getChild("group");
		if (group == null)
			condition.addContent(group = new Element("group").addContent(condition.removeContent()));
		for (Element action : actions)
			group.addContent(action);
	}

	/**
	 * Randomly fill <tt>n</tt> with integers from <tt>0 <= i < k</tt> without
	 * repeating.
	 * 
	 * @param n
	 *            the array to fill
	 * @param k
	 *            the upper bound
	 */
	static void choose(int[] n, int k) {
		ArrayList<Integer> ints = new ArrayList<Integer>();
		for (int i = 0; i < k; i++)
			ints.add(i);
		for (int i = 0; i < n.length; i++)
			n[i] = ints.remove(random.nextInt(ints.size()));
		Arrays.sort(n);
	}

	/**
	 * Randomly fill <tt>n</tt> with integers from <tt>0 <= i < k</tt>.
	 * 
	 * @param n
	 *            the array to fill
	 * @param k
	 *            the upper bound
	 */
	static void sample(int[] n, int k) {
		for (int i = 0; i < n.length; i++)
			n[i] = random.nextInt(k);
		Arrays.sort(n);
	}

	/**
	 * Normalize the values in <tt>n</tt> so they sum to approximately
	 * <tt>k</tt>
	 * 
	 * @param n
	 *            the array to normalize
	 * @param k
	 *            what the values of n should sum to
	 */
	static void normalize(int[] n, int k) {
		double sum = 0.0;
		for (int i : n)
			sum += i;
		System.out.println(sum);
		for (int i = 0; i < n.length; i++)
			n[i] = (int) Math.round(k * n[i] / sum);
	}

	/**
	 * Generates an input to the resource manager.
	 * 
	 * @param cm
	 *            the {@link ConfigurationModel} to generate resource
	 *            availability from
	 * @param dir
	 *            the directory to create the resouce file in
	 * @return the resource availability {@link File}
	 * @throws ConfigurationException
	 *             thrown when an error occurs during XML generation
	 */
	static File generateResources(ConfigurationModel cm, File dir) {
		try {
			Object t;
			int rate = (Integer) cm.getValue("rate");
			int hours = (Integer) cm.getValue("hours");
			int patients = rate * hours;
			t = cm.getValue("runs");
			int runs = t != null ? (Integer) t : 1;

			File result = File.createTempFile("ResourceAvailabilityInput", ".txt", dir);
			PrintStream out = new PrintStream(result, "ISO-8859-1");
			try {
				out.println("# Simulation round: 1");
				printRepeat(out, "Patient       ", patients, runs);
				printRepeat(out, "Doctor        ", (Integer) cm.getValue("doctor"), runs);
				printRepeat(out, "Nurse         ", (Integer) cm.getValue("nurse"), runs);
				printRepeat(out, "ExternalTriage", (Integer) cm.getValue("external triage"), runs);
				printRepeat(out, "InternalTriage", (Integer) cm.getValue("internal triage"), runs);
				printRepeat(out, "ExternalClerk ", (Integer) cm.getValue("external clerk"), runs);
				printRepeat(out, "InternalClerk ", (Integer) cm.getValue("internal clerk"), runs);
				printRepeat(out, "AnyBed        ", (Integer) cm.getValue("bed"), runs);
				printRepeat(out, "LabTech       ", (Integer) cm.getValue("lab tech"), runs);
				printRepeat(out, "CTScanTech    ", (Integer) cm.getValue("ctscan tech"), runs);
				printRepeat(out, "XrayTech      ", (Integer) cm.getValue("xray tech"), runs);
				printRepeat(out, "UltrasoundTech", (Integer) cm.getValue("ultrasound tech"), runs);
			} finally {
				out.close();
			}
			return result;
		} catch (UnsupportedEncodingException ex) {
			throw new ConfigurationException(ex);
		} catch (IOException ex) {
			throw new ConfigurationException(ex.getMessage());
		}
	}

	/**
	 * Print the name of a parameter, and then the amount a specific number of
	 * times.
	 * 
	 * @param out
	 *            where to print to
	 * @param s
	 *            the parameter to print
	 * @param n
	 *            the amount of the parameter
	 * @param r
	 *            the number of times to repeat it
	 */
	static void printRepeat(PrintStream out, String s, int n, int r) {
		out.print(s);
		for (int i = 0; i < r; i++) {
			out.print(" ");
			out.print(n);
		}
		out.println();
	}

	/**
	 * loads an XML document
	 * 
	 * @param file
	 *            the XML file to load
	 * @return the XML document
	 * @throws JDOMException
	 * @throws IOException
	 */
	static Document loadDocument(File file) throws JDOMException, IOException {
		return new SAXBuilder().build(file);
	}

	/**
	 * saves an XML document
	 * 
	 * @param file
	 *            the file to save the XML to
	 * @param doc
	 *            the XML document
	 * @throws IOException
	 */
	static void saveDocument(File file, Document doc) throws IOException {
		FileOutputStream fos = new FileOutputStream(file);
		try {
			new XMLOutputter(Format.getPrettyFormat()).output(doc, fos);
		} finally {
			fos.close();
		}
	}

	/**
	 * loads a properties file
	 * 
	 * @param file
	 *            the properties to load
	 * @return the properties
	 * @throws IOException
	 */
	static Properties loadProperties(File file) throws IOException {
		FileInputStream in = new FileInputStream(file);
		try {
			Properties props = new Properties();
			props.loadFromXML(in);
			return props;
		} finally {
			in.close();
		}
	}

	/**
	 * saves properties to a file
	 * 
	 * @param file
	 *            the file to save to
	 * @param props
	 *            the properties
	 * @throws IOException
	 */
	static void saveProperties(File file, Properties props) throws IOException {
		FileOutputStream out = new FileOutputStream(file);
		try {
			props.storeToXML(out, "Romeo Configuration for a specification simulation/execution", "UTF-8");
		} finally {
			out.close();
		}
	}
}
